# UndoManager.py
# This file is part of Labyrinth
#
# Copyright (C) 2006 - Don Scorgie <Don@Scorgie.org>
#
# Labyrinth is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# Labyrinth is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Labyrinth; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA  02110-1301  USA
#

import gtk

# Different modes of operation - redo, undo
UNDO = 0
REDO = 1

# Some general actions we should always expect.
# Some require special handling within ourselves
# (taking care of letter insertion / word insertion)

INSERT_LETTER = 100
INSERT_WORD = 101
DELETE_LETTER = 102
DELETE_WORD = 103
TRANSFORM_CANVAS = 104

class UndoAction:
	def __init__(self, owner, undo_type, callback, *args):
		self.owner = owner
		self.undo_type = undo_type
		self.callback = callback
		self.text = ""
		if undo_type == INSERT_LETTER or undo_type == INSERT_WORD or undo_type == DELETE_LETTER or \
		   undo_type == DELETE_WORD:
			for z in args:
				if isinstance(z, basestring):
					self.text = z
					break
		self.args = args
	def add_arg (self, *args):
		for t in args:
			self.args += (t,)

class UndoManager:
	''' A basic manager for undoing and redoing actions.\
		Doesn't do anything itself, instead it marshals \
		all the minion classes to do its bidding.  Any class \
		can add items to its lists and they're corresponding \
		methods will be called if and when needed.  The \
		manager doesn't care what you give it, so long as
		it has a method to call and an owner'''

	def __init__(self, top_owner, undo_widget = None, redo_widget = None):
		self.undo = undo_widget
		self.redo = redo_widget

		self.blocked = False

		self.undo_list = []
		self.redo_list = []

		if self.undo:
			self.undo.connect('activate', self.undo_action)
		if self.redo:
			self.redo.connect('activate', self.redo_action)
		self.owner = top_owner
		self.update_sensitive ()

	def block (self):
		''' Used as generally, when an undo is performed a \
		signal will be emitted that causes an undo action \
		to be added.  Use this to block tht from happening \
		To add actions again, call unblock'''
		self.blocked = True

	def unblock (self):
		self.blocked = False

	def set_widgets (self, undo, redo):
		self.undo = undo
		self.redo = redo
		self.undo.connect('activate', self.undo_action)
		self.redo.connect('activate', self.redo_action)
		self.update_sensitive ()

	def update_sensitive (self):
		if not self.undo or not self.redo:
			return
		if len(self.undo_list) > 0:
			self.undo.set_sensitive (True)
		else:
			self.undo.set_sensitive (False)
		if len(self.redo_list) > 0:
			self.redo.set_sensitive (True)
		else:
			self.redo.set_sensitive (False)

	def undo_action (self, arg):
		result = self.undo_list.pop()
		self.redo_list.append (result)
		self.update_sensitive ()
		result.callback (result, mode=UNDO)

	def redo_action (self, arg):
		result = self.redo_list.pop()
		self.undo_list.append (result)
		self.update_sensitive ()
		result.callback (result, mode=REDO)

	def combine_insertions (self, action):
		final_text = action.text
		start_iter = action.args[0]
		length = action.args[2]
		owner = action.owner
		cb = action.callback
		old_attrs = action.args[3]
		new_attrs = action.args[4]
		if len (self.undo_list) > 0:
			back = self.undo_list.pop ()
		else:
			self.undo_list.append (action)
			return
		add_back = True
		while back and back.owner == owner \
		      and (back.undo_type == INSERT_LETTER or back.undo_type == INSERT_WORD):
			if back.text.rfind(' ') != -1:
				break
			old_attrs = back.args[3]
			if back.args[0] <= start_iter:
				start_iter = back.args[0]
				final_text = back.text+final_text
			else:
				final_text += back.text
			length += back.args[2]
			if len (self.undo_list) == 0:
				add_back = False
				break
			back = self.undo_list.pop ()
		if add_back:
			self.undo_list.append (back)
		combi = UndoAction (owner, INSERT_WORD, cb, start_iter, final_text, length, old_attrs, new_attrs)
		self.undo_list.append (combi)

	def combine_deletions (self, action):
		bytes = True
		byte_collection = action.args[3]
		final_text = action.text
		start_iter = action.args[0]
		length = action.args[2]
		owner = action.owner
		cb = action.callback
		old_attrs = action.args[4]
		new_attrs = action.args[5]
		if len (self.undo_list) > 0:
			back = self.undo_list.pop ()
		else:
			self.undo_list.append (action)
			return
		add_back = True
		while back and back.owner == owner \
			  and (back.undo_type == DELETE_LETTER or back.undo_type == DELETE_WORD):
			if back.text.rfind(' ') != -1:
				break
			old_attrs = back.args[4]
			if back.args[0] <= start_iter:
				start_iter = back.args[0]
				final_text = back.text+final_text
				if bytes:
					byte_collection = back.args[3] + byte_collection
			else:
				final_text += back.text
				if bytes:
					byte_collection += back.args[3]
			length += back.args[2]
			if len (self.undo_list) == 0:
				add_back = False
				break
			back = self.undo_list.pop ()
		if add_back:
			self.undo_list.append (back)
		if bytes:
			combi = UndoAction (owner, DELETE_WORD, cb, start_iter, final_text, length, byte_collection, old_attrs, new_attrs)
		else:
			combi = UndoAction (owner, DELETE_WORD, cb, start_iter, final_text, length, -1, old_attrs, new_attrs)
		self.undo_list.append (combi)

	def combine_transforms (self, action):
		if len (self.undo_list) > 0:
			back = self.undo_list.pop ()
		else:
			self.undo_list.append (action)
			return
		add_back = True
		final_zoom = action.args[1]
		final_trans = action.args[3]
		orig_zoom = action.args[0]
		orig_trans = action.args[2]
		owner = action.owner
		cb = action.callback
		while back and back.owner == owner and \
			  back.undo_type == TRANSFORM_CANVAS:
			orig_zoom = back.args[0]
			orig_trans = back.args[2]			
			if len (self.undo_list) == 0:
				add_back = False
				break
			back = self.undo_list.pop ()
		if add_back:
			self.undo_list.append (back)
		self.undo_list.append (UndoAction (owner, TRANSFORM_CANVAS, cb, orig_zoom,
										   final_zoom, orig_trans, final_trans))


	def peak (self):
		if len (self.undo_list) > 0:
			return self.undo_list[-1]
		else:
			return UndoAction (None, None, None)
	def pop (self):
		if len (self.undo_list) > 0:
			return self.undo_list.pop ()
		else:
			return None

	def add_undo (self, action):
		if self.blocked:
			return
		if not isinstance(action, UndoAction):
			print "Error: Not a valid undo action.  Ignoring."
			return
		del self.redo_list[:]
		if action.undo_type == INSERT_LETTER:
			self.combine_insertions (action)
		elif action.undo_type == DELETE_LETTER:
			self.combine_deletions (action)
		elif action.undo_type == TRANSFORM_CANVAS:
			self.combine_transforms (action)
		else:
			self.undo_list.append (action)
		self.update_sensitive ()
